package com.dbflow5.processor.definition.provider;

import com.dbflow5.StringUtils;
import com.dbflow5.contentprovider.annotation.ContentProvider;
import com.dbflow5.contentprovider.annotation.TableEndpoint;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.MethodDefinition;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.Validators;
import com.dbflow5.processor.definition.BaseDefinition;
import com.dbflow5.processor.utils.ElementExtensions;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.*;

import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Description: Writes the [ContentProvider] class.
 */
public class ContentProviderDefinition extends BaseDefinition {

    public ContentProviderDefinition(ContentProvider provider, Element typeElement, ProcessorManager processorManager) {
        super(typeElement, processorManager, null);
        databaseTypeName = ProcessorUtils.extractTypeNameFromAnnotation(provider, e -> null, contentProvider -> {
            //contentProvider.database;
            return null;
        });
        authority = provider.authority();
        holderClass = ProcessorUtils.extractTypeNameFromAnnotation(provider, e -> null, contentProvider -> {
            //contentProvider.initializeHolderClass;
            return null;
        });

        // init
        setOutputClassName("_" + DEFINITION_NAME);

        Validators.TableEndpointValidator validator = new Validators.TableEndpointValidator();
        List<? extends Element> elements = manager.elements.getAllMembers((TypeElement)typeElement);
        elements.forEach(element -> {
            TableEndpoint tableEndpoint = element.getAnnotation(TableEndpoint.class);
            TableEndpointDefinition endpointDefinition = new TableEndpointDefinition(tableEndpoint, element, manager);
            if (validator.validate(processorManager, endpointDefinition)) {
                endpointDefinitions.add(endpointDefinition);
            }
        });

        if (!ProcessorUtils.isSubclass(ElementExtensions.toTypeElement(databaseTypeName, manager), manager.processingEnvironment,
                ClassNames.CONTENT_PROVIDER_DATABASE)) {
            manager.logError("A Content Provider database $elementClassName " +
                    "must extend ${ClassNames.CONTENT_PROVIDER_DATABASE}");
        }

        if (holderClass != TypeName.OBJECT &&
                !ProcessorUtils.isSubclass(ElementExtensions.toTypeElement(holderClass, manager), manager.processingEnvironment, ClassNames.DATABASE_HOLDER)) {
            manager.logError("The initializeHolderClass $holderClass must point to a subclass" +
                    "of ${ClassNames.DATABASE_HOLDER}");
        }
    }

    public TypeName databaseTypeName;
    public List<TableEndpointDefinition> endpointDefinitions = new ArrayList<>();

    private final String authority;
    private final TypeName holderClass;

    private final MethodDefinition[] methods = {new com.dbflow5.processor.definition.provider.ContentProvider.QueryMethod(this, manager),
            new com.dbflow5.processor.definition.provider.ContentProvider.InsertMethod(this, false),
            new com.dbflow5.processor.definition.provider.ContentProvider.InsertMethod(this, true),
            new com.dbflow5.processor.definition.provider.ContentProvider.DeleteMethod(this, manager),
            new com.dbflow5.processor.definition.provider.ContentProvider.UpdateMethod(this, manager)};


    @Override
    public TypeName extendsClass() {
        return ClassNames.BASE_CONTENT_PROVIDER;
    }

    @Override
    public void onWriteDefinition(TypeSpec.Builder typeBuilder) {
        if (holderClass != TypeName.OBJECT) {
            MethodSpec.Builder builder = MethodSpec.constructorBuilder();
            builder.addStatement("super($T.class)", holderClass);
            typeBuilder.addMethod(builder.build());
        }

        AtomicInteger code = new AtomicInteger();
        for (TableEndpointDefinition endpointDefinition : endpointDefinitions) {
            endpointDefinition.contentUriDefinitions.forEach(definition -> {
                FieldSpec.Builder fieldBuilder = FieldSpec.builder(TypeName.INT, definition.name)
                        .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL);
                fieldBuilder.initializer(CodeBlock.builder().add(String.valueOf(code.get())).build());

                typeBuilder.addField(fieldBuilder.build());
                code.getAndIncrement();
            });
        }

        FieldSpec.Builder fieldBuilder = FieldSpec.builder(ClassNames.URI_MATCHER, URI_MATCHER)
                .addModifiers(Modifier.PRIVATE, Modifier.FINAL);
        fieldBuilder.initializer(CodeBlock.builder().add("new $T($T.NO_MATCH)", ClassNames.URI_MATCHER, ClassNames.URI_MATCHER).build());
        typeBuilder.addField(fieldBuilder.build());


        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("onCreate")
                .returns(TypeName.BOOLEAN)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addAnnotation(Override.class)
                .addStatement("final $T $AUTHORITY = $L", String.class, authority.contains("R.string.")? "getContext().getString("+authority+")" : "\""+authority+"\"");

        for (TableEndpointDefinition endpointDefinition : endpointDefinitions) {
            endpointDefinition.contentUriDefinitions.forEach(definition -> {
                String path;
                if (!StringUtils.isNullOrEmpty(definition.path)) {
                    path = "\""+definition.path+"\"";
                } else {
                    path = CodeBlock.builder().add("$L.$L.getPath()", definition.elementClassName, definition.name).build().toString();
                }
                methodBuilder.addStatement("$L.addURI($L, $L, $L)", URI_MATCHER, AUTHORITY, path, definition.name);
            });
        }

        methodBuilder.addStatement("return super.onCreate()");
        typeBuilder.addMethod(methodBuilder.build());


        MethodSpec.Builder methodBuilder2 = MethodSpec.methodBuilder("getDatabaseName")
                .returns(String.class)
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addStatement("return $T.getDatabaseName($T.class)", ClassNames.FLOW_MANAGER, databaseTypeName);

        typeBuilder.addMethod(methodBuilder2.build());

        MethodSpec.Builder methodSpec3 = MethodSpec.methodBuilder("getType")
                .returns(String.class)
                .addParameter(ParameterSpec.builder(ClassNames.URI, "uri").build())
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

        CodeBlock.Builder codeBuilder = CodeBlock.builder();

        codeBuilder.addStatement("$T type = null", ClassName.get(String.class));
        codeBuilder.beginControlFlow("switch($L.match(uri))", URI_MATCHER);

        endpointDefinitions.forEach(it -> it.contentUriDefinitions.forEach(definition -> {
            codeBuilder.beginControlFlow("case $L:", definition.name);
            codeBuilder.addStatement("type = $S", definition.type);
            codeBuilder.addStatement("break");
        }));
        com.dbflow5.processor.definition.provider.ContentProvider.appendDefault(codeBuilder);
        codeBuilder.endControlFlow();

        codeBuilder.addStatement("return type");

        methodSpec3.addCode(codeBuilder.build());

        typeBuilder.addMethod(methodSpec3.build());

        for(MethodDefinition definition : methods) {
            if(definition != null) {
                typeBuilder.addMethod(definition.methodSpec());
            }
        }
    }

    public static final String DEFINITION_NAME = "Provider";
    public static final String URI_MATCHER = "MATCHER";
    private static final String AUTHORITY = "AUTHORITY";
}